/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.nageoffer.shortlink.admin.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.UUID;
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.nageoffer.shortlink.admin.common.biz.user.UserContext;
import com.nageoffer.shortlink.admin.common.convention.exception.ClientException;
import com.nageoffer.shortlink.admin.common.convention.exception.ServiceException;
import com.nageoffer.shortlink.admin.common.enums.UserErrorCodeEnum;
import com.nageoffer.shortlink.admin.dao.entity.UserDO;
import com.nageoffer.shortlink.admin.dao.mapper.UserMapper;
import com.nageoffer.shortlink.admin.dto.req.UserLoginReqDTO;
import com.nageoffer.shortlink.admin.dto.req.UserRegisterReqDTO;
import com.nageoffer.shortlink.admin.dto.req.UserUpdateReqDTO;
import com.nageoffer.shortlink.admin.dto.resp.UserLoginRespDTO;
import com.nageoffer.shortlink.admin.dto.resp.UserRespDTO;
import com.nageoffer.shortlink.admin.service.GroupService;
import com.nageoffer.shortlink.admin.service.UserService;
import lombok.RequiredArgsConstructor;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.BeanUtils;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

import static com.nageoffer.shortlink.admin.common.constant.RedisCacheConstant.LOCK_USER_REGISTER_KEY;
import static com.nageoffer.shortlink.admin.common.constant.RedisCacheConstant.USER_LOGIN_KEY;
import static com.nageoffer.shortlink.admin.common.enums.UserErrorCodeEnum.USER_EXIST;
import static com.nageoffer.shortlink.admin.common.enums.UserErrorCodeEnum.USER_NAME_EXIST;
import static com.nageoffer.shortlink.admin.common.enums.UserErrorCodeEnum.USER_SAVE_ERROR;

/**
 * 用户接口实现层
 */
@Service
@RequiredArgsConstructor
public class UserServiceImpl extends ServiceImpl<UserMapper, UserDO> implements UserService {

    private final RBloomFilter<String> userRegisterCachePenetrationBloomFilter;
    private final RedissonClient redissonClient;
    private final StringRedisTemplate stringRedisTemplate;
    private final GroupService groupService;

    /**
     * 根据用户名获取用户信息
     *
     * @param username 用户名，用于查询用户信息
     * @return UserRespDTO 用户响应DTO，包含用户信息
     * @throws ServiceException 当用户不存在时抛出服务异常
     */
    @Override
    public UserRespDTO getUserByUsername(String username) {
        // 创建查询条件，查询用户名与参数username相等的用户记录
        LambdaQueryWrapper<UserDO> queryWrapper = Wrappers.lambdaQuery(UserDO.class)
                .eq(UserDO::getUsername, username);
        // 执行查询，获取一条用户记录
        UserDO userDO = baseMapper.selectOne(queryWrapper);
        // 如果查询结果为空，抛出用户不存在的异常
        if (userDO == null) {
            throw new ServiceException(UserErrorCodeEnum.USER_NULL);
        }
        // 创建用户响应DTO对象
        UserRespDTO result = new UserRespDTO();
        // 将用户记录的属性复制到用户响应DTO中
        BeanUtils.copyProperties(userDO, result);
        // 返回用户响应DTO
        return result;
    }

    /**
     * 判断用户名是否已存在
     * 通过查询用户注册缓存的布隆过滤器来实现，以快速判断用户名是否存在
     * 使用布隆过滤器的好处是查询效率高，避免了直接访问数据库的开销
     *
     * @param username 待检查的用户名
     * @return 如果布隆过滤器中不包含该用户名，则返回true，表示用户名不存在；
     *          如果布隆过滤器中包含该用户名，则返回false，表示用户名可能已存在
     * 注意：由于布隆过滤器存在一定的误判率，返回false时用户名不一定真实存在，需要进一步确认
     */
    @Override
    public Boolean hasUsername(String username) {
        return !userRegisterCachePenetrationBloomFilter.contains(username);
    }

    /**
     * 用户注册方法
     *
     * 本方法主要用于处理用户注册请求，确保用户名唯一性，并在数据库中创建新用户
     * 使用了Redis分布式锁来防止并发注册相同用户名，以及事务管理来确保数据一致性
     *
     * @param requestParam 用户注册请求参数，包含用户名等信息
     * @throws ClientException 如果用户名已存在或用户保存失败，抛出此异常
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void register(UserRegisterReqDTO requestParam) {
        // 检查用户名是否已存在
        if (!hasUsername(requestParam.getUsername())) {
            throw new ClientException(USER_NAME_EXIST);
        }

        // 获取Redis分布式锁，防止并发注册
        RLock lock = redissonClient.getLock(LOCK_USER_REGISTER_KEY + requestParam.getUsername());
        if (!lock.tryLock()) {
            throw new ClientException(USER_NAME_EXIST);
        }

        try {
            // 插入新用户到数据库
            int inserted = baseMapper.insert(BeanUtil.toBean(requestParam, UserDO.class));
            if (inserted < 1) {
                throw new ClientException(USER_SAVE_ERROR);
            }

            // 为新用户创建默认分组
            groupService.saveGroup(requestParam.getUsername(), "默认分组");

            // 使用布隆过滤器缓存已注册用户名，减少数据库查询
            userRegisterCachePenetrationBloomFilter.add(requestParam.getUsername());
        } catch (DuplicateKeyException ex) {
            // 处理数据库唯一键冲突异常
            throw new ClientException(USER_EXIST);
        } finally {
            // 释放锁
            lock.unlock();
        }
    }

    /**
     * 更新用户信息
     *
     * 此方法用于处理用户信息的更新请求它首先检查请求更新的用户名是否与当前登录用户一致，
     * 以确保只有当前登录用户才能修改自己的信息如果用户名不匹配，则抛出客户端异常，
     * 表示修改请求异常如果用户名匹配，则将请求参数转换为用户实体，并使用条件更新包装器
     * 来更新数据库中的用户信息
     *
     * @param requestParam 用户更新请求数据传输对象，包含要更新的用户信息
     * @throws ClientException 如果请求更新的用户名与当前登录用户不一致，则抛出此异常
     */
    @Override
    public void update(UserUpdateReqDTO requestParam) {
        // 检查请求更新的用户名是否与当前登录用户一致
        if (!Objects.equals(requestParam.getUsername(), UserContext.getUsername())) {
            throw new ClientException("当前登录用户修改请求异常");
        }

        // 创建条件更新包装器，用于指定更新条件
        LambdaUpdateWrapper<UserDO> updateWrapper = Wrappers.lambdaUpdate(UserDO.class)
                .eq(UserDO::getUsername, requestParam.getUsername());

        // 将请求参数转换为用户实体，并根据条件更新数据库中的用户信息
        baseMapper.update(BeanUtil.toBean(requestParam, UserDO.class), updateWrapper);
    }

    /**
     * 用户登录方法
     *
     * 该方法实现用户登录功能，包括验证用户凭据、处理登录状态和生成登录令牌
     * 它首先通过查询数据库来验证用户是否存在并检查登录凭据，然后根据用户的登录状态
     * 在Redis中存储或更新登录信息，并生成一个唯一的登录令牌返回给客户端
     *
     * @param requestParam 用户登录请求参数，包含用户名和密码
     * @return 返回用户登录响应DTO，包含登录令牌
     * @throws ClientException 如果用户不存在或登录错误，抛出客户端异常
     */
    @Override
    public UserLoginRespDTO login(UserLoginReqDTO requestParam) {
        // 构建查询条件以验证用户凭据
        LambdaQueryWrapper<UserDO> queryWrapper = Wrappers.lambdaQuery(UserDO.class)
                .eq(UserDO::getUsername, requestParam.getUsername())
                .eq(UserDO::getPassword, requestParam.getPassword())
                .eq(UserDO::getDelFlag, 0);
        UserDO userDO = baseMapper.selectOne(queryWrapper);
        if (userDO == null) {
            throw new ClientException("用户不存在");
        }
        // 检查用户是否已经登录
        Map<Object, Object> hasLoginMap = stringRedisTemplate.opsForHash().entries(USER_LOGIN_KEY + requestParam.getUsername());
        if (CollUtil.isNotEmpty(hasLoginMap)) {
            // 如果用户已登录，延长登录状态的有效期并返回现有的令牌
            stringRedisTemplate.expire(USER_LOGIN_KEY + requestParam.getUsername(), 30L, TimeUnit.MINUTES);
            String token = hasLoginMap.keySet().stream()
                    .findFirst()
                    .map(Object::toString)
                    .orElseThrow(() -> new ClientException("用户登录错误"));
            return new UserLoginRespDTO(token);
        }
        // 如果用户未登录，生成新的令牌并存储登录状态到Redis
        String uuid = UUID.randomUUID().toString();
        stringRedisTemplate.opsForHash().put(USER_LOGIN_KEY + requestParam.getUsername(), uuid, JSON.toJSONString(userDO));
        stringRedisTemplate.expire(USER_LOGIN_KEY + requestParam.getUsername(), 30L, TimeUnit.MINUTES);
        return new UserLoginRespDTO(uuid);
    }

    /**
     * 检查用户登录状态
     *
     * @param username 用户名，用于标识用户
     * @param token 用户登录时生成的唯一令牌，用于验证用户登录状态
     * @return Boolean 如果用户已登录且令牌有效，则返回true，否则返回false
     */
    @Override
    public Boolean checkLogin(String username, String token) {
        // 通过Redis中的HASH结构检查用户登录状态
        // 使用用户登录键和用户名构建键名，检查给定token是否存在
        return stringRedisTemplate.opsForHash().get(USER_LOGIN_KEY + username, token) != null;
    }

    /**
     * 退出登录功能
     *
     * 本方法旨在处理用户退出登录的请求通过验证用户的身份和登录状态，
     * 然后删除用户的登录信息，以确保用户成功登出
     *
     * @param username 用户名，用于标识用户
     * @param token 用户登录时获取的令牌，用于验证用户登录状态
     * @throws ClientException 当用户Token不存在或用户未登录时抛出此异常
     */
    @Override
    public void logout(String username, String token) {
        // 检查用户是否已登录
        if (checkLogin(username, token)) {
            // 删除Redis中的用户登录信息，以实现用户退出登录
            stringRedisTemplate.delete(USER_LOGIN_KEY + username);
            return;
        }
        // 如果用户未登录或Token无效，抛出异常
        throw new ClientException("用户Token不存在或用户未登录");
    }
}
